Borland Online And The Cobb Group Present:


April, 1994 - Vol. 1 No. 4

Borland C++ 4.0 Tip - Using the extended diagnostic macros

In last month's Borland C++ Developer's Journal, we showed you how to use the TRACE() and WARN() diagnostic macros that ship with the Borland C++ 4.0 package. You can use these macros to display text messages and conditional warnings from debug versions of your application.

Unfortunately, the TRACE() and WARN() macros are all-or-nothing options. If you define the __TRACE constant when you build the application, the application will display every TRACE message that appears in the code when you run the application.

If you're looking for only one or two particular TRACE messages, you may have difficulty finding the message you want when it's surrounded by less important ones. This same problem may occur when you use the WARN() macro and define the __WARN constant.

However, you can use the extended diagnostic macros TRACEX() and WARNX() to display a specific type of message without displaying other types. In this article, we'll show you how to create and display different types of diagnostic messages by using the extended diagnostic macros.

Diagnostic groups and messages

To control the display of a particular type of diagnostic message, you'll need to define a diagnostic group. A diagnostic group manages a set of diagnostic messages. After you define a diagnostic group, you can create messages that belong to that group.

When you create a message for a given group, you'll also specify the default message level for that group. You can think of a given message's level as its priority within that diagnostic group. When you're using the extended diagnostic macros, messages with a lower numeric level have a higher priority. To determine which messages from a diagnostic group the program will display, you can set that group's level to any value between 0 and 127.

Before you can use the extended diagnostic macros in a given source file, you'll need to add an #include directive either for the CHECKS.H header file or for a header file that contains an #include directive for the CHECKS.H file. CHECKS.H defines the default and extended diagnostic macro classes and functions. In addition, this file also defines the DIAG_DEFINE_GROUP() macro you'll use to define a new diagnostic group.

Defining diagnostic groups

In your source file, you'll use the macro DIAG_DEFINE_GROUP() ahead of statements that use any of the extended diagnostic macro functions. You'll use this macro to name the diagnostic group and set its initial message level.

At runtime, each diagnostic group keeps track of its current level setting as well as an Enabled flag. The extended diagnostic macros use the Enabled flag to determine whether they'll display messages that belong to this particular group. If the Enabled flag is set to 1 (True), the extended diagnostic macros will display messages whose levels are equal to or lower than their group's current level setting.

For example, if you want to create a diagnostic group named Global to display messages that relate to global variables, you would add

DIAG_DEFINE_GROUP(Global, 1, 0);

to your source file. In this statement, Global is the name of the group, 1 is the initial setting for the group's Enabled flag, and 0 is the initial value for the group's message level.

Creating extended messages

Initializing a global variable is an important action, so you'll probably want to display a message at the initialization point if you're viewing any other messages from the Global diagnostic group. If you want to display a message whenever you enable its diagnostic group, set the message's level to 0.

To create a TRACEX message that precedes initialization of the global variable array1, you'd put a line similar to

TRACEX(Global, 0, "Initializing array1");

before the line that actually initializes array1. In this macro call, Global specifies the diagnostic group, 0 is the message's level, and "Initializing array1" is the message that TRACEX() will display.

Modifying a global variable's value is also important, but less so than initializing. If you want a diagnostic message to precede code that modifies the global variable userCount, you can create a message similar to the following line:

TRACEX(Global, 1, "Modifying userCount");

To force the compiler to expand the TRACEX() macro, you need to define the global constant __TRACE. Then, when you're ready to run this code in a debugging mode, you can set the Global diagnostic group's message level to 0 in the DIAG_DEFINE_GROUP() macro to make the program display only the initialization message.

If you decide later that you also want to see the message about modifying the global variable, you'll need to set the default message level higher for the Global diagnostic group. If you change the level parameter to 1 in the DIAG_DEFINE_GROUP() macro and then rebuild the program, it will display both messages.

By default, the CHECKS.H header file defines the default diagnostic group Def with the Enabled flag set to 1 and the message level set to 0. This file defines the standard TRACE() and WARN() macros as part of the Def diagnostic group. In fact, the CHECKS.H header file contains a macro that the compiler uses to expand the macro call

TRACE("HI")

into the extended macro call

TRACEX(Def, 0, "HI")

Since the default group's message level is 0 and its Enabled flag is set to 1, TRACE() macros will display their text message whenever you use them in a file that defines the global constant __TRACE. However, the diagnostic group Def is just another group with a special name. As we'll see later, you can manipulate the Def diagnostic group's Enabled flag and message level just as you would for groups you define.

Creating extended warnings

The CHECKS.H header file also defines an extended version of the WARN() macro. The WARNX() macro displays a diagnostic group message only when its second parameter is 0. To force the compiler to expand the WARNX() macro, you need to define the global constant __WARN.

The WARNX() macro's first parameter is the message's diagnostic group, as was the case for the TRACEX() macro. However, since the warning condition is now the second parameter, the message level and message text parameters move to the third and fourth positions respectively.

For example, to see a warning message when your program sets the global variable count to 0, you can add the line

WARNX(Global, count, 1, "count == 0");

This macro call will display its message only when you enable the Global diagnostic group, you set the Global group's message level to 1 or higher, and the variable count is equal to 0.

Runtime changes

So far, we've discussed the enabled/disabled state and message level of a diagnostic group as static settings. We've described changing the message level of a group in the initial definition macro and then recompiling the program.

However, you can also have your program change the message level or Enabled flag at runtime. Table A summarizes the extended diagnostic function macros you'll use to adjust these parameters.

Table A - You can use the extended diagnostic function macros to manipulate a diagnostic group's parameters.
Extended Diagnostic Function Macros
DIAG_ENABLE() Sets the Enabled flag for a given group
DIAG_ISENABLED() Returns the state of the Enabled flag for a given group
DIAG_SETLEVEL() Sets the value of the Level parameter for a given group
DIAG_GETLEVEL() Returns the value of the Level parameter for a given group

When you use the DIAG_ENABLE() and DIAG_SETLEVEL() function macros, you'll pass two parameters: the diagnostic group's name and the new state or message level. For example, to change the current message level of the Global group to 5, you would call the DIAG_SETLEVEL() function macro like this:

DIAG_SETLEVEL(Global, 5);

You can use the DIAG_ISENABLED() and DIAG_GETLEVEL() function macros as if they were normal function calls. To display the current message level for the Global group, you could use the DIAG_GETLEVEL() function macro in a statement similar to

cout << "Global level is ";
cout << DIAG_GETLEVEL(Global) << endl;

If you use any of the extended macro function calls, be sure to include the line

#if defined(__TRACE) || defined(__WARN)

before code that uses the macro, and

#endif

after the code. Using these conditional compile directives will force the compiler to skip the debug-related code that appears between the #if and #endif directives.

Putting the extended diagnostic macros to work

Now, let's create a DOS program that displays messages from the extended diagnostic macro TRACEX(). To demonstrate the extended diagnostic function macros, we'll use them to adjust the types of messages the program will display.

To begin, launch the Borland C++ 4.0 Integrated Development Environment. When the menu bar and desktop appear, choose New Project... from the Project menu. After the New Project dialog box appears, enter the following name in the Project Path and Name entry field:

\bc4\ext_diag\ext_diag.ide

In the Platform combo box, choose DOS Standard as the type of application. Then, choose Small from the Target Model combo box as the memory model size for this project. To allow the compiler to link the standard C/C++ runtime library file, select the Runtime check box in the Standard Libraries group. Now you can create the new project by clicking OK.

When the EXT_DIAG.IDE Project window appears, double-click on the EXT_DIAG.CPP file's icon. When the file's editor window appears, enter the code from Listing A.


Listing A: EXT_DIAG.CPP

#define __TRACE
#include <checks.h>

DIAG_DEFINE_GROUP(Func, 1, 0);

int foo(int var)
{
    TRACEX(Func, 1, "> foo()");
    TRACEX(Func, 2, "var = " << var);

  int returnValue = var * 5;

    TRACEX(Func, 2, "returning "
           << returnValue);
    TRACEX(Func, 1, "< foo()");

  return returnValue;
}

int main()
{
#if defined(__TRACE) || defined(__WARN)
  int dataLevel = 0;
  int codeLevel = 0;
#endif

  char exitCode = 0;

  while((exitCode != 'y') && 
	(exitCode != 'Y'))
  {
    TRACE("Default Trace");
    TRACEX(Def, 1, "Level 1 Def");
    TRACEX(Def, 3, "Level 3 Def");

    foo(10);

#if defined(__TRACE) || defined(__WARN)
    cout << endl << endl;
    if((int)(DIAG_ISENABLED(Def)))
    {
      cout << "data diagnostic level ";
      cout << (int)DIAG_GETLEVEL(Def) <<
		endl;
    }
    else
      cout << "data diagnostics disabled\n";
    cout << "set diagnostic level?";
    cin >> dataLevel;
    if(dataLevel < 0)
      DIAG_ENABLE(Def, 0);
    else
    {
      DIAG_ENABLE(Def, 1);
      DIAG_SETLEVEL(Def, dataLevel);
    }

    if((int)DIAG_ISENABLED(Func))
    {
      cout << "code diagnostic level ";
      cout << (int)DIAG_GETLEVEL(Func) <<
		endl;
    }
    else
      cout << "code diagnostics disabled\n";
    cout << "set diagnostic level?";
    cin >> codeLevel;
    if(codeLevel < 0)
      DIAG_ENABLE(Func, 0);
    else
    {
      DIAG_ENABLE(Func, 1);
      DIAG_SETLEVEL(Func, codeLevel);
    }
#endif

    cout << endl << "exit (Y or N)?";
    cin >> exitCode;
  }

  return 0;
}

When you finish entering the code for EXT_DIAG.CPP, save it by choosing Save from the File menu. Now, let's run the program to see the extended diagnostic macros in action.

Running DIAG.CPP

To run this project, double-click on the EXT_DIAG.IDE project's icon in the Project window. This tells the compiler to compile the EXT_DIAG.IDE source file, link the resulting OBJ file with the appropriate library files, and then run the program from a DOS prompt.

When the program runs, it will enter the while() loop and display the following:

c:>Trace EXT_DIAG.CPP 30: [Def] Default Trace
data diagnostic level 0 set diagnostic level?

Enter 5 at the prompt. Next, you'll see

code diagnostic level 0
set diagnostic level?

Enter 5 at this prompt as well. The program now moves to the bottom of the while() loop and displays

exit (Y or N)?

Enter N. This causes the program to re-enter the while() loop and now display

Trace EXT_DIAG.CPP 30: [Def] Default Trace
Trace EXT_DIAG.CPP 31: [Def] Level 1 Def
Trace EXT_DIAG.CPP 32: [Def] Level 3 Def
Trace EXT_DIAG.CPP 8: [Func] > foo()
Trace EXT_DIAG.CPP 9: [Func] var = 10
Trace EXT_DIAG.CPP 13: [Func] returning 50
Trace EXT_DIAG.CPP 14: [Func] < foo()

data diagnostic level 5
set diagnostic level?

This time through, enter 1 as the data diagnostic level and 1 as the code diagnostic level. When the exit prompt reappears, enter N again and you'll see

Trace EXT_DIAG.CPP 30: [Def] Default Trace
Trace EXT_DIAG.CPP 31: [Def] Level 1 Def
Trace EXT_DIAG.CPP 8: [Func] > foo()
Trace EXT_DIAG.CPP 14: [Func] < foo()

data diagnostic level 1
set diagnostic level?

If you look closely at the output above, you'll notice that the program skipped the TRACE message from line 32 because the level for the Def diagnostic group was too low. Likewise, the program skipped the messages from lines 9 and 13 that are level 2 messages for the Func diagnostic group.

Now, enter -1 for the data diagnostic level and 3 for the code diagnostic level, and then enter N at the exit prompt. During this pass, the program will display

Trace EXT_DIAG.CPP 8: [Func] > foo()
Trace EXT_DIAG.CPP 9: [Func] var = 10
Trace EXT_DIAG.CPP 13: [Func] returning 50
Trace EXT_DIAG.CPP 14: [Func] < foo()
data diagnostic level 5 set diagnostic level?

This time, the program skipped all the messages from the Def group. This is because the -1 parameter forces the lines

if(dataLevel < 0)
  DIAG_ENABLE(Def, 0);

from our code to disable the Def diagnostic group by setting its Enabled flag to 0. To exit EXT_DIAG.EXE, enter any value for the diagnostic levels and then enter Y at the exit prompt.

Other considerations

If you decide to use the extended diagnostic macros in your projects, you may want to declare an enumeration for the different diagnostic levels. By declaring an enumeration like

const enum {FINAL, BETA2, BETA1, ALPHA1};

you can use the enumeration value in place of the level parameter in any of the extended diagnostic macros. This will make it easier for someone to know whether a given message is really appropriate for a particular debugging session.

You can create a keyboard macro for function bodies that automatically inserts entry and exit messages at the beginning and end of each function. By doing this, you'll be more likely to include some form of diagnostic message for each function.

Conclusion

While the TRACE() and WARN() macros are useful, the extended macros TRACEX() and WARNX() provide more display options for large projects. In addition, by using the extended function macros, you can control the diagnostic message output dynamically at runtime.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.